package org.hackystat.ambientdevice.server;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.logging.Logger;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.hackystat.ambientdevice.ConfigurationException;
import org.hackystat.ambientdevice.device.AmbientDevice;
import org.hackystat.ambientdevice.device.DeviceFactory;
import org.hackystat.ambientdevice.server.jaxb.Configuration;
import org.hackystat.ambientdevice.server.jaxb.Device;
import org.hackystat.ambientdevice.server.jaxb.Project;
import org.hackystat.ambientdevice.server.jaxb.Trigger;
import org.hackystat.ambientdevice.server.jaxb.Action;
import org.hackystat.ambientdevice.server.jaxb.TriggerActionPair;
import org.hackystat.ambientdevice.server.jaxb.User;
import org.hackystat.ambientdevice.trigger.TriggerFactory;
import org.hackystat.dailyprojectdata.client.DailyProjectDataClient;
import org.hackystat.sensorbase.client.SensorBaseClient;
import org.hackystat.utilities.logger.HackystatLogger;
import org.xml.sax.SAXException;

/**
 * Server class to periodically retrieve data from hackystat severs and change ambient device's
 * state accordingly.
 * 
 * @author Shaoxuan Zhang
 * @author Sonwright M. Gomez
 * @author Edward Kim
 * 
 */
public class AmbientServer {

  /** the time of a millisecond. */
  public static final long MILLISECOND = 1000;
  /** The list of trigger action timer tasks. */
  private List<TriggerActionTimerTask> timersTaskList = new ArrayList<TriggerActionTimerTask>();
  /** The Timer that control the triggers. */
  private Timer timer = new Timer();
  /** Map from a name to a Device. */
  private Map<String, AmbientDevice> deviceMap = new HashMap<String, AmbientDevice>();
  /** user. */
  private User user;
  /** the SensorBaseClient. */
  private SensorBaseClient sensorBaseClient;
  /** the DailyProjectDataClient. */
  private DailyProjectDataClient dailyProjectDataClient;
  /** The logger for logging. */
  private Logger logger = HackystatLogger.getLogger("org.hackystat.ambientdevice");
  /** Path of the configuration. */
  private String configurationFilePath = null;
  /** Flag for debug mode. */
  private boolean debug = false;
  /** Configuration that contains everything. **/
  private Configuration configuration = null;
  /** List of projects. */
  private List<Project> projects = new ArrayList<Project>();
  /** Store the user's name or email. */
  private String username;

  /**
   * Construct this instance with jaxb class that unmarshelled from the configuration xml file.
   * 
   * @param filePath the relative file path of the configuration file
   */
  public AmbientServer(String filePath) {
    if (filePath != null && !filePath.startsWith(System.getProperty("file.separator"))) {
      filePath = System.getProperty("file.separator") + filePath;
    }
    filePath = System.getProperty("user.dir") + filePath;
    logger.fine("Loading configuration from " + filePath);
    setConfigurationFilePath(filePath);
    logger.fine("Configuration Loaded.");
  }

  /**
   * Construct a default AmbientServer with nothing in it.
   */
  public AmbientServer() {
    //default constructor that does nothing.
  }

  /**
   * Main method.
   * 
   * @param args command line arguments.
   * @throws JAXBException when JAXB encounter errors
   * @throws SAXException If a SAX error occurs during parsing.
   */
  public static void main(String[] args) throws JAXBException, SAXException {
    if (args.length < 1) {
      System.out.println("configuration xml file is needed to run the server.");
      System.exit(1);
    }
    AmbientServer server = new AmbientServer(args[args.length - 1]);
    if (args.length >= 2) {
      for (int i = 0; i < args.length - 1; ++i) {
        if ("-verbose".equals(args[i])) {
          HackystatLogger.setLoggingLevel(server.getLogger(), "FINER");
        }
        else if ("-debug".equals(args[i])) {
          server.setDebug(true);
          HackystatLogger.setLoggingLevel(server.getLogger(), "FINEST");
        }
      }
    }
    server.start();
  }

  /**
   * Start the server.
   * 
   * @return the timer tasks that scheduled into timer.
   * @throws JAXBException for some errors
   */
  public int start() throws JAXBException {
    this.initialize();
    for (TriggerActionTimerTask timerTask : this.getTriggerActionTimerTask()) {
      timer.schedule(timerTask, 0, timerTask.getInterval());
    }
    logger.finer(timersTaskList.size() + " TriggerActionTimerTasks are registered.");
    logger.fine("Server is up.");
    return this.timersTaskList.size();
  }

  /**
   * Start the server for the UI and returns true for a successful start.
   * 
   * @param username the username
   * @param password the password
   * @return the timer tasks that scheduled into timer.
   */
  public boolean uiStart(String username, String password) {
    String filePath = "";
    this.username = username.replace('@','_').replace('.', '_');
    
    filePath = "\\xml\\users\\" + this.username + ".xml";
    filePath = System.getProperty("user.dir") + filePath;
    File file = new File(filePath);
    System.out.println(filePath);
    if (!file.exists()) {
      return false;
    }

    if (filePath != null && !filePath.startsWith(System.getProperty("file.separator"))) {
      filePath = System.getProperty("file.separator") + filePath;
    }
    setConfigurationFilePath(filePath);
    try {
      this.initialize();
      for (TriggerActionTimerTask timerTask : this.getTriggerActionTimerTask()) {
        timer.schedule(timerTask, 0, timerTask.getInterval());
      }
      return true;
    }

    catch (Exception e) {
      e.printStackTrace();
      this.timersTaskList = null;
      return false;
    }
  }

  /**
   * Start the server for the UI and returns true for a successful start.
   * 
   * @param timersTaskList2 the timersTaskList2 
   * @return the timer tasks that scheduled into timer.
   * @throws JAXBException for some errors
   */
  public int start(List<TriggerActionTimerTask> timersTaskList2) throws JAXBException {
    this.timersTaskList = timersTaskList2;
    for (TriggerActionTimerTask timerTask : this.getTriggerActionTimerTask()) {
      timer.schedule(timerTask, 0, timerTask.getInterval());
    }
    logger.finer(timersTaskList2.size() + " TriggerActionTimerTasks are registered.");
    logger.fine("Server is up.");
    return this.timersTaskList.size();
  }

  /**
   * Shutdown the server by canceling all scheduled tasks.
   */
  public void shutdown() {
    timer.cancel();
    timer.purge();
  }

  /**
   * @return the list of timer tasks.
   */
  public List<TriggerActionTimerTask> getTriggerActionTimerTask() {
    return this.timersTaskList;
  }

  /**
   * Initialize this server using the configuration file indicated with the file path.
   * 
   * @throws JAXBException when JAXB encounter errors
   * @return boolean to tell if it init. right 
   */
  private boolean initialize() throws JAXBException {
    boolean validate = true;
    //Configuration configuration = null;
    logger.finest("Reading information from configuration XML file.");
    configuration = this.loadConfiguration(getConfigurationFilePath());
    this.user = configuration.getUser();
    this.sensorBaseClient = new SensorBaseClient(configuration.getServers().getSensorBaseHost(),
        this.user.getEmail(), this.user.getPassword());
    this.dailyProjectDataClient = new DailyProjectDataClient(configuration.getServers()
        .getDailyProjectDataHost(), this.user.getEmail(), this.user.getPassword());
    //load devices
    DeviceFactory deviceFactory = new DeviceFactory();
    for (Device device : configuration.getDevices().getDevice()) {
      try {
        AmbientDevice ambientDevice = deviceFactory.getDevice(device);
        if (this.deviceMap.containsValue(ambientDevice)) {
          throw new ConfigurationException("Duplicated devices with the same Device ID/serial"
              + " are found.");
        }
        if (this.deviceMap.containsKey(device.getName())) {
          throw new ConfigurationException("Duplicated devices with the same name are found.");
        }
        this.deviceMap.put(device.getName(), ambientDevice);
      }
      catch (ConfigurationException e) {
        logger.warning(e.getMessage());
        if (isDebug()) {
          e.printStackTrace();
        }
        validate = false;
      }
    }
    TriggerFactory triggerFactory = new TriggerFactory(this.sensorBaseClient,
        this.dailyProjectDataClient);
    //load trigger action pair.
    projects = configuration.getProjects().getProject();
    for (Project project : configuration.getProjects().getProject()) {
      for (TriggerActionPair pair : project.getTriggerActionPairs().getTriggerActionPair()) {
        TriggerActionTimerTask task = new TriggerActionTimerTask();
        //set interval
        task.setInterval(pair.getInterval() * MILLISECOND);
        task.setName(pair.getId());
        //set action
        for (Trigger trigger : pair.getTriggers().getTrigger()) {
          try {
            task.getTriggerList().add(triggerFactory.getTrigger(project, trigger));
          }
          catch (ConfigurationException e) {
            logger.warning(e.getMessage());
            if (isDebug()) {
              e.printStackTrace();
            }
            validate = false;
          }
        }
        //set positive action
        if (pair.getPositiveActions() != null) {
          for (Action action : pair.getPositiveActions().getAction()) {
            try {
              task.getPositiveActionList().add(
                  this.getDeviceMap().get(action.getDeviceName()).getAction(action));
            }
            catch (ConfigurationException e) {
              logger.warning(e.getMessage());
              if (isDebug()) {
                e.printStackTrace();
              }
              validate = false;
            }
            catch (NullPointerException e1) {
              logger.warning("Device with name " + action.getDeviceName() + " is not found.");
            }
          }
        }
        //set negative action
        if (pair.getNegativeActions() != null) {
          for (Action action : pair.getNegativeActions().getAction()) {
            try {
              task.getNegativeActionList().add(
                  this.getDeviceMap().get(action.getDeviceName()).getAction(action));
            }
            catch (ConfigurationException e) {
              logger.warning(e.getMessage());
              if (isDebug()) {
                e.printStackTrace();
              }
              validate = false;
            }
            catch (NullPointerException e1) {
              logger.warning("Device with name " + action.getDeviceName() + " is not found.");
            }
          }
        }
        //add the task to system.
        this.timersTaskList.add(task);
      }
    }
    return validate;
  }

  /**
   * Load the xml file to JAXB generated class.
   * 
   * @param configFilePath the file path of the configuration file
   * @return the JAXB generated class
   * @throws JAXBException when jaxb unmarshall error occur
   */
  private Configuration loadConfiguration(String configFilePath) throws JAXBException {
    JAXBContext configurationJaxbContext = JAXBContext
        .newInstance("org.hackystat.ambientdevice.server.jaxb");
    File configFile = new File(configFilePath);
    Unmarshaller unmarshaller = configurationJaxbContext.createUnmarshaller();
    //Add schema check.
    SchemaFactory schemaFactory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
    String schemaPath = System.getProperty("user.dir") + "/xml/schema/configuration.xsd";
    File schemaFile = new File(schemaPath.replace("/", System.getProperty("file.separator")));
    Schema schema;
    try {
      schema = schemaFactory.newSchema(schemaFile);
    }
    catch (SAXException e) {
      e.printStackTrace();
      return null;
    }
    unmarshaller.setSchema(schema);
    //Unmarshal
    return (Configuration) unmarshaller.unmarshal(configFile);
  }

  /**
   * @return AmbientDevice the ambient device
   * @param deviceName the name of the device
   */
  public AmbientDevice getDeviceByName(String deviceName) {
    return this.getDeviceMap().get(deviceName);
  }

  /**
   * @return Configuration the configuration of this server
   */
  public Configuration getConfiguration() {
    return this.configuration;
  }

  /**
   * @param deviceMap the deviceMap to set
   */
  public final void setDeviceMap(Map<String, AmbientDevice> deviceMap) {
    this.deviceMap = deviceMap;
  }

  /**
   * @return the deviceMap
   */
  public final Map<String, AmbientDevice> getDeviceMap() {
    return deviceMap;
  }

  /**
   * @param user the user to set
   */
  public final void setUser(User user) {
    this.user = user;
  }

  /**
   * @return the user
   */
  public final User getUser() {
    return user;
  }

  /**
   * @param sensorBaseClient the sensorBaseClient to set
   */
  public final void setSensorBaseClient(SensorBaseClient sensorBaseClient) {
    this.sensorBaseClient = sensorBaseClient;
  }

  /**
   * @return the sensorBaseClient
   */
  public final SensorBaseClient getSensorBaseClient() {
    return sensorBaseClient;
  }

  /**
   * @param dailyProjectDataClient the dailyProjectDataClient to set
   */
  public final void setDailyProjectDataClient(DailyProjectDataClient dailyProjectDataClient) {
    this.dailyProjectDataClient = dailyProjectDataClient;
  }

  /**
   * @return the dailyProjectDataClient
   */
  public final DailyProjectDataClient getDailyProjectDataClient() {
    return dailyProjectDataClient;
  }

  /**
   * @param logger the logger to set
   */
  public final void setLogger(Logger logger) {
    this.logger = logger;
  }

  /**
   * @return the logger
   */
  public final Logger getLogger() {
    return logger;
  }

  /**
   * @param configurationFilePath the configurationFilePath to set
   */
  public final void setConfigurationFilePath(String configurationFilePath) {
    this.configurationFilePath = configurationFilePath;
  }

  /**
   * @return the configurationFilePath
   */
  public final String getConfigurationFilePath() {
    return configurationFilePath;
  }

  /**
   * @param debug the debug to set
   */
  public final void setDebug(boolean debug) {
    this.debug = debug;
  }

  /**
   * @return the debug
   */
  public final boolean isDebug() {
    return debug;
  }

  /**
   * @return the projects
   */
  public List<Project> getProjects() {
    return projects;
  }
}
